热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

时会|可能会_Java网络编程——NIO处理写事件(SelectionKey.OP_WRITE)

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java网络编程——NIO处理写事件(SelectionKey.OP_WRITE)相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java网络编程——NIO处理写事件(SelectionKey.OP_WRITE)相关的知识,希望对你有一定的参考价值。


在前面NIO的例子中,在服务端,有对连接事件(SelectionKey.OP_ACCEPT)的处理,也有对读事件(SelectionKey.OP_READ)的处理,但是没有对写事件(SelectionKey.OP_WRITE)进行处理,原因就是写事件有些特殊,在这里单独记录一下。

网上有一些例子都是在服务端读完数据后直接给客户端SocketChannel对应的SelectionKey注册上写事件(SelectionKey.OP_WRITE),写完数据后也不注销写事件:

@Slf4j
public class Nioserver
public static void main(String[] args) throws Exception
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true)
int select = selector.select();
Set<SelectionKey> selectionKeys &#61; selector.selectedKeys();
Iterator<SelectionKey> iterator &#61; selectionKeys.iterator();
while (iterator.hasNext())
SelectionKey selectionKey &#61; iterator.next();
if (selectionKey.isAcceptable())
ServerSocketChannel serverSocket &#61; (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel &#61; serverSocket.accept();
log.info("receive connection from client. client:", socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
else if (selectionKey.isReadable())
ByteBuffer byteBuffer &#61; ByteBuffer.allocate(1024);
SocketChannel socketChannel &#61; (SocketChannel) selectionKey.channel();
socketChannel.read(byteBuffer);
String message &#61; new String(byteBuffer.array()).trim();
byteBuffer.clear();
log.info("receive message from client. client: message length:", socketChannel.getRemoteAddress(),message.length());
socketChannel.register(selectionKey.selector(), SelectionKey.OP_WRITE);
selectionKey.attach(message);
else if (selectionKey.isWritable())
SocketChannel socketChannel &#61; (SocketChannel) selectionKey.channel();
String response &#61; (String) selectionKey.attachment();
ByteBuffer byteBuffer &#61; ByteBuffer.allocate(response.length());
byteBuffer.put(response.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
log.info("send message to client. client: message length:", socketChannel.getRemoteAddress(), response.length());

iterator.remove();




但实际上根本不能这么用&#xff0c;即使在服务端向客户端写完数据后&#xff0c;也会不断触发写事件&#xff08;selector.select()返回&#xff0c; selectionKey.isWritable()返回true &#xff09;。



什么时候会触发写事件呢&#xff1f;

在前面的文章 《Java网络编程——NIO的阻塞IO模式、非阻塞IO模式、IO多路复用模式的使用》 中简单提到SelectionKey.OP_WRITE事件表示已经可以向通道写数据了&#xff08;通道目前可以用于写操作&#xff09;&#xff0c;那什么时候才算“可以向通道写数据”呢&#xff1f;

如果有channel在Selector上注册了SelectionKey.OP_WRITE事件&#xff0c;在调用selector.select();时&#xff0c;系统会检查内核写缓冲区是否可写&#xff08;当写缓冲区已满、channel调用了shutdownOutPut等情况&#xff0c;内核缓冲区不可写&#xff09;&#xff0c;如果可写&#xff0c;selector.select();会立即返回写事件。

把服务端处理读事件的代码优化如下&#xff1a;

&#64;Slf4j
public class NIOSelectorNonblockingWriteServer
private final static int MESSAGE_LENGTH &#61; 1024 * 1024 * 100;
public static void main(String[] args) throws Exception
ServerSocketChannel serverSocketChannel &#61; ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8080), 50);
Selector selector &#61; Selector.open();
SelectionKey serverSocketKey &#61; serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true)
int count &#61; selector.select();
log.info("select event count:" &#43; count);
Set<SelectionKey> selectionKeys &#61; selector.selectedKeys();
Iterator<SelectionKey> iterator &#61; selectionKeys.iterator();
while (iterator.hasNext())
SelectionKey selectionKey &#61; iterator.next();
// 有客户端请求建立连接
if (selectionKey.isAcceptable())
handleAccept(selectionKey);

// 有客户端发送数据
else if (selectionKey.isReadable())
handleRead(selectionKey);

// 可以向客户端发送数据
else if (selectionKey.isWritable())
handleWrite(selectionKey);

iterator.remove();



private static void handleAccept(SelectionKey selectionKey) throws IOException
ServerSocketChannel serverSocketChannel &#61; (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel &#61; serverSocketChannel.accept();
if (Objects.nonNull(socketChannel))
log.info("receive connection from client. client:", socketChannel.getRemoteAddress());
// 设置客户端Channel为非阻塞模式&#xff0c;否则在执行socketChannel.read()时会阻塞
socketChannel.configureBlocking(false);
Selector selector &#61; selectionKey.selector();
socketChannel.register(selector, SelectionKey.OP_READ);


private static void handleRead(SelectionKey selectionKey) throws IOException
SocketChannel socketChannel &#61; (SocketChannel) selectionKey.channel();
ByteBuffer readBuffer &#61; ByteBuffer.allocate(MESSAGE_LENGTH);
int length &#61; 0;
while (length < MESSAGE_LENGTH)
length &#43;&#61; socketChannel.read(readBuffer);

log.info("receive message from client. client: message length:", socketChannel.getRemoteAddress(), readBuffer.position());
ByteBuffer writeBuffer &#61; ByteBuffer.allocate(readBuffer.position());
readBuffer.flip();
writeBuffer.put(readBuffer);
// 读完数据后&#xff0c;为 SelectionKey 注册可写事件
if (!isInterest(selectionKey, SelectionKey.OP_WRITE))
selectionKey.interestOps(selectionKey.interestOps() &#43; SelectionKey.OP_WRITE);

writeBuffer.flip();
selectionKey.attach(writeBuffer);

// 服务端可能是为每个Channel维护一块缓冲区&#xff0c;当向某个Channel写数据时缓冲区满了&#xff0c;还可以向其他Channel写数据
private static void handleWrite(SelectionKey selectionKey) throws IOException
SocketChannel socketChannel &#61; (SocketChannel) selectionKey.channel();
ByteBuffer writeBuffer &#61; (ByteBuffer) selectionKey.attachment();
int writeLength &#61; socketChannel.write(writeBuffer);
log.info("send message to client. client: message length:", socketChannel.getRemoteAddress(), writeLength);
if (!writeBuffer.hasRemaining())
// 写完数据后&#xff0c;要把写事件取消&#xff0c;否则当写缓冲区有剩余空间时&#xff0c;会一直触发写事件
selectionKey.interestOps(selectionKey.interestOps() - SelectionKey.OP_WRITE);
// socketChannel.shutdownOutput(); // channel调用shutdownOutput()后&#xff0c;会停止触发写事件


// 判断 SelectionKey 对某个事件是否感兴趣
private static boolean isInterest(SelectionKey selectionKey, int event)
int interestSet &#61; selectionKey.interestOps();
boolean isInterest &#61; (interestSet & event) &#61;&#61; event;
return isInterest;



在服务端读取完客户端消息后&#xff0c;会先判断当前SelectionKey是否已经注册了写事件&#xff0c;如果没有则为其注册写事件。在服务端向客户端写完数据后&#xff0c;会取消写事件。

在上面判断SelectionKey是否已经注册了某个事件时&#xff0c;判断条件是 selectionKey.interestOps() & SelectionKey.OP_WRITE &#xff0c;selectionKey.interestOps()就是已经注册的事件&#xff0c;SelectionKey中可以只用1个整形数字来表示多个注册的事件&#xff08;interestOps变量&#xff09;&#xff0c;SelectionKey.OP_READ&#61;1&#xff08;二进制为 00000001&#xff09;&#xff0c;SelectionKey.OP_WRITE&#61;4&#xff08;二进制为 00000100&#xff09;&#xff0c;SelectionKey.OP_CONNECT&#61;8&#xff08;二进制为 00001000&#xff09;&#xff0c;SelectionKey.OP_ACCEPT&#61;16&#xff08;二进制为 00010000&#xff09;。当注册某个事件时&#xff0c;会把对应事件对应的整数&#xff08;单个事件对应的整数或者多个事件对应的整数和&#xff09;赋值给interestOps变量&#xff0c;比如注册读事件时&#xff0c;interestOps&#61;1&#xff1b;注册读事件&#43;写事件时&#xff0c;interestOps&#61;1&#43;4 。因此可以通过按位与的算法来判断当前SelectionKey是否注册过某个事件。



为什么不在处理完读事件&#xff08;selectionKey.isReadable()&#xff09;后&#xff0c;直接写数据&#xff0c;而要重新注册一个读事件呢&#xff1f;

那我们就继续看这个例子&#xff1a;

&#64;Slf4j
public class NIOSelectorBlockingWriteServer
private final static int MESSAGE_LENGTH &#61; 1024 * 1024 * 100;
public static void main(String[] args) throws Exception
ServerSocketChannel serverSocketChannel &#61; ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8080), 50);
Selector selector &#61; Selector.open();
SelectionKey serverSocketKey &#61; serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true)
int count &#61; selector.select();
log.info("select event count:" &#43; count);
Set<SelectionKey> selectionKeys &#61; selector.selectedKeys();
Iterator<SelectionKey> iterator &#61; selectionKeys.iterator();
while (iterator.hasNext())
SelectionKey selectionKey &#61; iterator.next();
// 有客户端请求建立连接
if (selectionKey.isAcceptable())
handleAccept(selectionKey);

// 有客户端发送数据
else if (selectionKey.isReadable())
handleRead(selectionKey);

iterator.remove();



private static void handleAccept(SelectionKey selectionKey) throws IOException
ServerSocketChannel serverSocketChannel &#61; (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel &#61; serverSocketChannel.accept();
if (Objects.nonNull(socketChannel))
log.info("receive connection from client. client:", socketChannel.getRemoteAddress());
// 设置客户端Channel为非阻塞模式&#xff0c;否则在执行socketChannel.read()时会阻塞
socketChannel.configureBlocking(false);
Selector selector &#61; selectionKey.selector();
socketChannel.register(selector, SelectionKey.OP_READ);


private static void handleRead(SelectionKey selectionKey) throws IOException
SocketChannel socketChannel &#61; (SocketChannel) selectionKey.channel();
ByteBuffer readBuffer &#61; ByteBuffer.allocate(MESSAGE_LENGTH);
int length &#61; 0;
while (length < MESSAGE_LENGTH)
length &#43;&#61; socketChannel.read(readBuffer);

log.info("receive message from client. client: message length:", socketChannel.getRemoteAddress(), readBuffer.position());
ByteBuffer writeBuffer &#61; ByteBuffer.allocate(readBuffer.position());
readBuffer.flip();
writeBuffer.put(readBuffer);
writeBuffer.flip();
while (writeBuffer.hasRemaining())
int writeLength &#61; socketChannel.write(writeBuffer);
log.info("send message to client. client: message length:", socketChannel.getRemoteAddress(), writeLength);




  • Debug模式启动服务端NIOSelectorBlockingWriteServer&#xff0c;在最后一行log.info("send message to client. client ……打上断点
  • Debug模式运行上面客户端NIOClient的例子&#xff0c;向服务端发送100MB的数据&#xff0c;在ByteBuffer readBuffer &#61; ByteBuffer.allocate(MESSAGE_LENGTH);打上断点&#xff0c;运行到断点时&#xff0c;当服务端调用了socketChannel.write(writeBuffer);向客户端发送的数据还没来得及被客户端接收完时&#xff08;这时候客户端卡在断点&#xff0c;并没有开始接收服务端的数据&#xff09;&#xff0c;数据会在服务端写缓冲区积压&#xff0c;在极限情况下&#xff0c;当服务写端缓冲区写满时&#xff0c;再调用socketChannel.write(writeBuffer);就写不进去了&#xff08;返回0&#xff09;。所以如果在处理完读事件后直接发送数据&#xff0c;遇到服务端写缓冲区满的情况时&#xff0c;会直接阻塞当前线程&#xff08;比如这个例子中会不断执行int writeLength &#61; socketChannel.write(writeBuffer);并返回0&#xff09;&#xff0c;无法及时处理其他客户端请求。
  • 这时再起一个线程&#xff0c;以Debug模式运行NIOClient&#xff0c;当执行socketChannel.write(writeBuffer);向服务端写数据时&#xff0c;可能会阻塞&#xff08;如果发送的数据量太大导致服务端接收缓存写满&#xff0c;此时服务端线程又在死循环&#xff0c;所以无法读取客户端发来的数据&#xff09;&#xff0c;如果这里不阻塞&#xff0c;执行到socketChannel.read(readBuffer)就会阻塞&#xff08;因为服务端一直没有读取到该客户端发送的数据&#xff0c;肯定也还没执行向客户端发送数据的代码&#xff09;。这里的客户端是以阻塞模式运行的&#xff0c;即使把客户端的SocketChannel设置为非阻塞模式&#xff0c;也是无法及时收到服务端返回的数据的。

所以在传输数据量较大&#xff0c;需要向客户端回写数据时&#xff0c;最好注册一个写事件&#xff0c;避免服务端缓冲区写满时导致线程阻塞&#xff0c;而无法及时处理其他事件的情况。这样才能体现NIO多路复用模式的特点&#xff0c;才能可以让一个线程同时为多个客户端服务。





转载请注明出处——胡玉洋 《Java网络编程——NIO处理写事件(SelectionKey.OP_WRITE)》


推荐阅读
  • http:my.oschina.netleejun2005blog136820刚看到群里又有同学在说HTTP协议下的Get请求参数长度是有大小限制的,最大不能超过XX ... [详细]
  • 本文介绍了在Linux下安装和配置Kafka的方法,包括安装JDK、下载和解压Kafka、配置Kafka的参数,以及配置Kafka的日志目录、服务器IP和日志存放路径等。同时还提供了单机配置部署的方法和zookeeper地址和端口的配置。通过实操成功的案例,帮助读者快速完成Kafka的安装和配置。 ... [详细]
  • 本文介绍了在rhel5.5操作系统下搭建网关+LAMP+postfix+dhcp的步骤和配置方法。通过配置dhcp自动分配ip、实现外网访问公司网站、内网收发邮件、内网上网以及SNAT转换等功能。详细介绍了安装dhcp和配置相关文件的步骤,并提供了相关的命令和配置示例。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • 配置IPv4静态路由实现企业网内不同网段用户互访
    本文介绍了通过配置IPv4静态路由实现企业网内不同网段用户互访的方法。首先需要配置接口的链路层协议参数和IP地址,使相邻节点网络层可达。然后按照静态路由组网图的操作步骤,配置静态路由。这样任意两台主机之间都能够互通。 ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
author-avatar
继续不插电的名单
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有